core: Add size information to commit metadata
authorJeremy Whiting <jeremy.whiting@collabora.com>
Wed, 9 Oct 2013 02:44:39 +0000 (20:44 -0600)
committerColin Walters <walters@verbum.org>
Sat, 19 Oct 2013 15:56:51 +0000 (11:56 -0400)
Add a --generate-sizes option to commit to add size information to the
commit metadata.  This will be used by higher level code which wants
to determine the total size necessary for downloading.

Makefile-tests.am
src/libostree/ostree-repo-commit.c
src/libostree/ostree-repo-private.h
src/libostree/ostree-repo.c
src/libostree/ostree-repo.h
src/ostree/ot-builtin-commit.c
tests/test-sizes.js [new file with mode: 0644]

index ef757712f9dc793d1cabc9b73f8edeaf8c1a3114..b634bc4329f830a6ade1d688652c7b241c0ce755 100644 (file)
@@ -80,9 +80,10 @@ testmeta_DATA += test-varint.test
 
 if BUILDOPT_GJS
 insttest_SCRIPTS += tests/test-core.js \
+       tests/test-sizes.js \
        tests/test-sysroot.js \
        $(NULL)
-testmeta_DATA += test-core.test test-sysroot.test
+testmeta_DATA += test-core.test test-sizes.test test-sysroot.test
 endif
 
 endif
index 6a4ceb73f3032dcb9b80aa1836e02941ffdc408f..5251840f82ed83d82b5e89376ff24071a24e1dae 100644 (file)
@@ -32,6 +32,7 @@
 #include "ostree-repo-file-enumerator.h"
 #include "ostree-checksum-input-stream.h"
 #include "ostree-mutable-tree.h"
+#include "ostree-varint.h"
 
 gboolean
 _ostree_repo_ensure_loose_objdir_at (int             dfd,
@@ -170,6 +171,129 @@ commit_loose_object_trusted (OstreeRepo        *self,
   return ret;
 }
 
+typedef struct
+{
+  gsize unpacked;
+  gsize archived;
+} OstreeContentSizeCacheEntry;
+
+static OstreeContentSizeCacheEntry *
+content_size_cache_entry_new (gsize unpacked,
+                              gsize archived)
+{
+  OstreeContentSizeCacheEntry *entry = g_slice_new0 (OstreeContentSizeCacheEntry);
+
+  entry->unpacked = unpacked;
+  entry->archived = archived;
+
+  return entry;
+}
+
+static void
+content_size_cache_entry_free (gpointer entry)
+{
+  if (entry)
+    g_slice_free (OstreeContentSizeCacheEntry, entry);
+}
+
+static void
+repo_store_size_entry (OstreeRepo       *self,
+                       const gchar      *checksum,
+                       gsize             unpacked,
+                       gsize             archived)
+{
+  if (G_UNLIKELY (self->object_sizes == NULL))
+    self->object_sizes = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                                g_free, content_size_cache_entry_free);
+
+  g_hash_table_replace (self->object_sizes,
+                        g_strdup (checksum),
+                        content_size_cache_entry_new (unpacked, archived));
+}
+
+static int
+compare_ascii_checksums_for_sorting (gconstpointer  a_pp,
+                                     gconstpointer  b_pp)
+{
+  char *a = *((char**)a_pp);
+  char *b = *((char**)b_pp);
+
+  return strcmp (a, b);
+}
+
+/**
+ * Create sizes metadata GVariant and add it to the metadata variant given.
+*/
+static gboolean
+add_size_index_to_metadata (OstreeRepo        *self,
+                            GVariant          *original_metadata,
+                            GVariant         **out_metadata,
+                            GCancellable      *cancellable,
+                            GError           **error)
+{
+  gboolean ret = FALSE;
+  gs_unref_variant_builder GVariantBuilder *builder = NULL;
+    
+  if (original_metadata)
+    {
+      builder = ot_util_variant_builder_from_variant (original_metadata, G_VARIANT_TYPE ("a{sv}"));
+    }
+  else
+    {
+      builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));
+    }
+
+  if (self->object_sizes &&
+      g_hash_table_size (self->object_sizes) > 0)
+    {
+      GHashTableIter entries = { 0 };
+      gchar *e_checksum = NULL;
+      OstreeContentSizeCacheEntry *e_size = NULL;
+      GVariantBuilder index_builder;
+      guint i;
+      gs_unref_ptrarray GPtrArray *sorted_keys = NULL;
+      
+      g_hash_table_iter_init (&entries, self->object_sizes);
+      g_variant_builder_init (&index_builder,
+                              G_VARIANT_TYPE ("a" _OSTREE_OBJECT_SIZES_ENTRY_SIGNATURE));
+
+      /* Sort the checksums so we can bsearch if desired */
+      sorted_keys = g_ptr_array_new ();
+      while (g_hash_table_iter_next (&entries,
+                                     (gpointer *) &e_checksum,
+                                     (gpointer *) &e_size))
+        g_ptr_array_add (sorted_keys, e_checksum);
+      g_ptr_array_sort (sorted_keys, compare_ascii_checksums_for_sorting);
+
+      for (i = 0; i < sorted_keys->len; i++)
+        {
+          guint8 csum[32];
+          const char *e_checksum = sorted_keys->pdata[i];
+          GString *buffer = g_string_new (NULL);
+
+          ostree_checksum_inplace_to_bytes (e_checksum, csum);
+          g_string_append_len (buffer, (char*)csum, 32);
+
+          e_size = g_hash_table_lookup (self->object_sizes, e_checksum);
+          _ostree_write_varuint64 (buffer, e_size->archived);
+          _ostree_write_varuint64 (buffer, e_size->unpacked);
+
+          g_variant_builder_add (&index_builder, "@ay",
+                                 ot_gvariant_new_bytearray ((guint8*)buffer->str, buffer->len));
+          g_string_free (buffer, TRUE);
+        }
+      
+      g_variant_builder_add (builder, "{sv}", "ostree.sizes",
+                             g_variant_builder_end (&index_builder));
+    }
+    
+  ret = TRUE;
+  *out_metadata = g_variant_builder_end (builder);
+  g_variant_ref_sink (*out_metadata);
+
+  return ret;
+}
+
 static gboolean
 write_object (OstreeRepo         *self,
               OstreeObjectType    objtype,
@@ -198,6 +322,8 @@ write_object (OstreeRepo         *self,
   gboolean temp_file_is_regular;
   gboolean is_symlink = FALSE;
   char loose_objpath[_OSTREE_LOOSE_PATH_MAX];
+  gsize unpacked_size = 0;
+  gboolean indexable = FALSE;
 
   g_return_val_if_fail (self->in_transaction, FALSE);
   
@@ -278,6 +404,9 @@ write_object (OstreeRepo         *self,
           gs_unref_object GConverter *zlib_compressor = NULL;
           gs_unref_object GOutputStream *compressed_out_stream = NULL;
 
+          if (self->generate_sizes)
+            indexable = TRUE;
+
           if (!gs_file_open_in_tmpdir_at (self->tmp_dir_fd, 0644,
                                           &temp_filename, &temp_out,
                                           cancellable, error))
@@ -298,10 +427,10 @@ write_object (OstreeRepo         *self,
               /* Don't close the base; we'll do that later */
               g_filter_output_stream_set_close_base_stream ((GFilterOutputStream*)compressed_out_stream, FALSE);
               
-              if (g_output_stream_splice (compressed_out_stream, file_input, 0,
-                                          cancellable, error) < 0)
+              unpacked_size = g_output_stream_splice (compressed_out_stream, file_input,
+                                                      0, cancellable, error);
+              if (unpacked_size < 0)
                 goto out;
-
             }
         }
       else
@@ -341,6 +470,18 @@ write_object (OstreeRepo         *self,
         }
     }
           
+  if (indexable)
+    {
+      gsize archived_size;
+      gs_unref_object GFileInfo *compressed_info =
+        g_file_query_info (temp_file, G_FILE_ATTRIBUTE_STANDARD_SIZE, 0,
+                           cancellable, error);
+      if (!compressed_info)
+        goto out;
+      archived_size = g_file_info_get_size (compressed_info);
+      repo_store_size_entry (self, actual_checksum, unpacked_size, archived_size);
+    }
+
   if (!_ostree_repo_has_loose_object (self, actual_checksum, objtype,
                                       &have_obj, loose_objpath,
                                       cancellable, error))
@@ -1238,15 +1379,21 @@ ostree_repo_write_commit (OstreeRepo      *self,
   gboolean ret = FALSE;
   gs_free char *ret_commit = NULL;
   gs_unref_variant GVariant *commit = NULL;
+  gs_unref_variant GVariant *new_metadata = NULL;
   gs_free guchar *commit_csum = NULL;
   GDateTime *now = NULL;
   OstreeRepoFile *repo_root = OSTREE_REPO_FILE (root);
 
   g_return_val_if_fail (subject != NULL, FALSE);
 
+  /* Add sizes information to our metadata object */
+  if (!add_size_index_to_metadata (self, metadata, &new_metadata,
+                                   cancellable, error))
+    goto out;
+
   now = g_date_time_new_now_utc ();
   commit = g_variant_new ("(@a{sv}@ay@a(say)sst@ay@ay)",
-                          metadata ? metadata : create_empty_gvariant_dict (),
+                          new_metadata ? new_metadata : create_empty_gvariant_dict (),
                           parent ? ostree_checksum_to_bytes_v (parent) : ot_gvariant_new_bytearray (NULL, 0),
                           g_variant_new_array (G_VARIANT_TYPE ("(say)"), NULL, 0),
                           subject, body ? body : "",
@@ -1526,6 +1673,11 @@ write_directory_to_mtree_internal (OstreeRepo                  *self,
 
   g_debug ("Examining: %s", gs_file_get_path_cached (dir));
 
+  if (modifier && modifier->flags & OSTREE_REPO_COMMIT_MODIFIER_FLAGS_GENERATE_SIZES)
+    {
+      self->generate_sizes = TRUE;
+    }
+
   /* If the directory is already in the repository, we can try to
    * reuse checksums to skip checksumming. */
   if (OSTREE_IS_REPO_FILE (dir) && modifier == NULL)
index ddd2c7a5cd503d1ec1318586d32c86b42410a0cb..0a3b0a017834f521d062b18de357f159eb456070 100644 (file)
@@ -24,6 +24,8 @@
 
 G_BEGIN_DECLS
 
+#define _OSTREE_OBJECT_SIZES_ENTRY_SIGNATURE "ay"
+
 /**
  * OstreeRepo:
  *
@@ -57,10 +59,12 @@ struct OstreeRepo {
   gboolean in_transaction;
   GHashTable *loose_object_devino_hash;
   GHashTable *updated_uncompressed_dirs;
+  GHashTable *object_sizes;
 
   GKeyFile *config;
   OstreeRepoMode mode;
   gboolean enable_uncompressed_cache;
+  gboolean generate_sizes;
 
   OstreeRepo *parent_repo;
 };
index 34bd1e88cda9b145239274d6d517fa83d87a1b84..e7a26bc49e29fa4c57c73ba1d4fc647b4d2022d9 100644 (file)
@@ -116,6 +116,7 @@ ostree_repo_finalize (GObject *object)
   g_clear_pointer (&self->txn_refs, g_hash_table_destroy);
   g_clear_pointer (&self->cached_meta_indexes, (GDestroyNotify) g_ptr_array_unref);
   g_clear_pointer (&self->cached_content_indexes, (GDestroyNotify) g_ptr_array_unref);
+  g_clear_pointer (&self->object_sizes, (GDestroyNotify) g_hash_table_unref);
   g_mutex_clear (&self->cache_lock);
   g_mutex_clear (&self->txn_stats_lock);
 
index 0aa4a3fd72bb8ecb960ed9e9e1d096dbc4291022..4f3e9d6ccabdffef2d5565f6db74729489aef3e6 100644 (file)
@@ -281,7 +281,8 @@ typedef OstreeRepoCommitFilterResult (*OstreeRepoCommitFilter) (OstreeRepo    *r
  */
 typedef enum {
   OSTREE_REPO_COMMIT_MODIFIER_FLAGS_NONE = 0,
-  OSTREE_REPO_COMMIT_MODIFIER_FLAGS_SKIP_XATTRS = (1 << 0)
+  OSTREE_REPO_COMMIT_MODIFIER_FLAGS_SKIP_XATTRS = (1 << 0),
+  OSTREE_REPO_COMMIT_MODIFIER_FLAGS_GENERATE_SIZES = (1 << 1)
 } OstreeRepoCommitModifierFlags;
 
 /**
index e9c030d7827500246a8306326e665384769c6ab4..6f63205617183f6a4bf7418d2a4cf2edd6417865 100644 (file)
@@ -45,6 +45,7 @@ static gboolean opt_table_output;
 static char **opt_key_ids;
 static char *opt_gpg_homedir;
 #endif
+static gboolean opt_generate_sizes;
 
 static GOptionEntry options[] = {
   { "subject", 's', 0, G_OPTION_ARG_STRING, &opt_subject, "One line subject", "subject" },
@@ -65,6 +66,7 @@ static GOptionEntry options[] = {
   { "gpg-sign", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_key_ids, "GPG Key ID to sign the commit with", "key-id"},
   { "gpg-homedir", 0, 0, G_OPTION_ARG_STRING, &opt_gpg_homedir, "GPG Homedir to use when looking for keyrings", "homedir"},
 #endif
+  { "generate-sizes", 0, 0, G_OPTION_ARG_NONE, &opt_generate_sizes, "Generate size information along with commit metadata", NULL },
   { NULL }
 };
 
@@ -284,6 +286,7 @@ ostree_builtin_commit (int argc, char **argv, OstreeRepo *repo, GCancellable *ca
   gs_unref_object OstreeMutableTree *mtree = NULL;
   gs_free char *tree_type = NULL;
   gs_unref_hashtable GHashTable *mode_adds = NULL;
+  OstreeRepoCommitModifierFlags flags = 0;
   OstreeRepoCommitModifier *modifier = NULL;
   OstreeRepoTransactionStats stats;
 
@@ -319,12 +322,17 @@ ostree_builtin_commit (int argc, char **argv, OstreeRepo *repo, GCancellable *ca
       goto out;
     }
 
-  if (opt_owner_uid >= 0 || opt_owner_gid >= 0 || opt_statoverride_file != NULL
+  if (opt_no_xattrs)
+    flags |= OSTREE_REPO_COMMIT_MODIFIER_FLAGS_SKIP_XATTRS;
+  if (opt_generate_sizes)
+    flags |= OSTREE_REPO_COMMIT_MODIFIER_FLAGS_GENERATE_SIZES;
+
+  if (flags != 0
+      || opt_owner_uid >= 0
+      || opt_owner_gid >= 0
+      || opt_statoverride_file != NULL
       || opt_no_xattrs)
     {
-      OstreeRepoCommitModifierFlags flags = 0;
-      if (opt_no_xattrs)
-        flags |= OSTREE_REPO_COMMIT_MODIFIER_FLAGS_SKIP_XATTRS;
       modifier = ostree_repo_commit_modifier_new (flags, commit_filter, mode_adds, NULL);
     }
 
diff --git a/tests/test-sizes.js b/tests/test-sizes.js
new file mode 100644 (file)
index 0000000..5cf765f
--- /dev/null
@@ -0,0 +1,82 @@
+#!/usr/bin/env gjs
+//
+// Copyright (C) 2013 Colin Walters <walters@verbum.org>
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; either
+// version 2 of the License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the
+// Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+// Boston, MA 02111-1307, USA.
+
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const OSTree = imports.gi.OSTree;
+
+function assertEquals(a, b) {
+    if (a != b)
+       throw new Error("assertion failed " + JSON.stringify(a) + " == " + JSON.stringify(b));
+}
+
+let testDataDir = Gio.File.new_for_path('test-data');
+testDataDir.make_directory(null);
+testDataDir.get_child('some-file').replace_contents("hello world!", null, false, 0, null);
+testDataDir.get_child('another-file').replace_contents("hello world again!", null, false, 0, null);
+
+let repoPath = Gio.File.new_for_path('repo');
+let repo = OSTree.Repo.new(repoPath);
+repo.create(OSTree.RepoMode.ARCHIVE_Z2, null);
+
+repo.open(null);
+
+let commitModifier = OSTree.RepoCommitModifier.new(OSTree.RepoCommitModifierFlags.GENERATE_SIZES, null);
+
+assertEquals(repo.get_mode(), OSTree.RepoMode.ARCHIVE_Z2);
+
+repo.prepare_transaction(null);
+
+let mtree = OSTree.MutableTree.new();
+repo.write_directory_to_mtree(testDataDir, mtree, commitModifier, null);
+let [,dirTree] = repo.write_mtree(mtree, null);
+let [,commit] = repo.write_commit(null, 'Some subject', 'Some body', null, dirTree, null);
+print("commit => " + commit);
+
+repo.commit_transaction(null, null);
+
+// Test the sizes metadata
+let [,commitVariant] = repo.load_variant(OSTree.ObjectType.COMMIT, commit);
+let metadata = commitVariant.get_child_value(0);
+let sizes = metadata.lookup_value('ostree.sizes', GLib.VariantType.new('aay'));
+let nSizes = sizes.n_children();
+assertEquals(nSizes, 2);
+let expectedUncompressedSizes = [12, 18];
+let foundExpectedUncompressedSizes = 0;
+for (let i = 0; i < nSizes; i++) {
+    let sizeEntry = sizes.get_child_value(i).deep_unpack();
+    assertEquals(sizeEntry.length, 34);
+    let compressedSize = sizeEntry[32];
+    let uncompressedSize = sizeEntry[33];
+    print("compressed = " + compressedSize);
+    print("uncompressed = " + uncompressedSize);
+    for (let j = 0; j < expectedUncompressedSizes.length; j++) {
+       let expected = expectedUncompressedSizes[j];
+       if (expected == uncompressedSize) {
+           print("Matched expected uncompressed size " + expected);
+           expectedUncompressedSizes.splice(j, 1);
+           break;
+       }
+    }
+}
+if (expectedUncompressedSizes.length > 0) {
+    throw new Error("Failed to match expectedUncompressedSizes: " + JSON.stringify(expectedUncompressedSizes));
+}
+
+print("test-sizes complete");